package com.googlecode.cswish.util.bean;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.persistence.CascadeType;
import javax.persistence.OneToMany;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.collection.spi.PersistentCollection;
import org.hibernate.proxy.HibernateProxy;

import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.struts.spring.Executor;
import com.googlecode.cswish.struts.spring.GenericService;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.bean.ModelScanner.ScannerCallback;
import com.googlecode.cswish.util.bean.ModelScanner.ScannerFlag;

public class DeepUpdateScannerCallback implements ScannerCallback {
	
	private static final Log LOGGER = LogFactory.getLog(DeepUpdateScannerCallback.class);
	
	private final static GenericService genericService = (GenericService) ObjectFactory.getInstance().getBean(GenericService.class, false);
	
	// only the update the enabled fields when enabledFields is defined
	private Collection<String> enabledFields;
	// treat it as changed field when the field value is null
	private Collection<String> changedFields;
	private Map<Object, Object> identityMap;
	
	private Executor<ScannerContext, ScannerContext> entityInterceptor;
	
	// main purpose: it's used to update the object reference
	private ScannerCallback referenceScannerCallback = new ScannerCallback() {
		@Override
		public ScannerFlag proceed(ScannerContext scannerContext) {
			if (scannerContext.isSimpleObj()) {
				return ScannerFlag.SCANSIBLING;
			} else {
				Object propertyValue = scannerContext.getSrcPropertyValue();
				Object newInstance = identityMap.get(propertyValue);
				if (newInstance == null) {
					// continue to scan the child element, but don't scan hibernate object
					if (propertyValue == null || propertyValue instanceof PersistentCollection 
							|| HibernateProxy.class.isAssignableFrom(propertyValue.getClass())) {
						return ScannerFlag.SCANSIBLING;
					} else {
						Object id = getId(propertyValue);
						boolean isHibernateModel = id == null;
						if (!isHibernateModel) {
							Object existed = genericService.load(ClassUtil.getClass(propertyValue), id);
							if (existed == propertyValue) {
								isHibernateModel = true;
							}
						}
						
						if (isHibernateModel) {
							return ScannerFlag.SCANSIBLING;
						} else {
							return ScannerFlag.SCANCHILD;
						}
					}
				} else {
					// update the reference (it's existed value , don't scan the children)
					scannerContext.setDestPropertyValue(newInstance);
					return ScannerFlag.SCANSIBLING;
				}
			}
		}
	};
	
	public DeepUpdateScannerCallback(Executor<ScannerContext, ScannerContext> entityInterceptor, 
			Collection<String> enabledFields, Collection<String> changedFields, Map<Object, Object> identityMap) {
		super();
		this.entityInterceptor = entityInterceptor;
		this.enabledFields = enabledFields;
		this.changedFields = changedFields;
		this.identityMap = identityMap;
	}

	public ScannerFlag proceed(ScannerContext scannerContext) {
		// skip the property which has the proxy method
		PropertyInfo propertyInfo = scannerContext.getPropertyDescriptor();
		// Only scan db column and wrapper method
		if (!propertyInfo.isTableColumn()) {
			return ScannerFlag.SCANSIBLING;
		}
		if (enabledFields != null && !enabledFields.contains(scannerContext.getChildPath())) {
			return ScannerFlag.SCANSIBLING;
		}
		
		if (entityInterceptor != null && !scannerContext.isSimpleObj()) {
			entityInterceptor.execute(scannerContext);
		}
		// proceed
		ScannerFlag scannerFlag = ScannerFlag.SCANSIBLING;
		if (scannerContext.getSrcPropertyValue() == null) {
			cleanTargetValue(scannerContext);
		} else if (scannerContext.isSimpleObj() || scannerContext.getPropertyDescriptor().getTypeClass().isArray()) {
			scannerContext.setDestPropertyValue(scannerContext.getSrcPropertyValue());
		} else if (scannerContext.isCollection()) {
			Collection<Object> fromColl = (Collection<Object>) scannerContext.getSrcPropertyValue();
			if (!(fromColl instanceof PersistentCollection)) {		// don't update if the source list is the hibernate collection				
				Collection<Object> destColl = (Collection<Object>) scannerContext.getDestPropertyValue();
				List<Object> updateList = new ArrayList<Object>();
				Collection<Object> combinedColl = caculateCollection(scannerContext, fromColl, destColl, updateList);
				scannerContext.setDestPropertyValue(combinedColl);
				ScannerContext childContext = null;
				if (entityInterceptor != null) {
					childContext = new ScannerContext(scannerContext);
					childContext.setParentPath(scannerContext.getChildPath());
					// scannerContext.setModelInfo(modelInfo);
				}
				
				for (int i = 0, len = updateList.size(); i < len; i++,i++) {
					Object src = updateList.get(i);
					Object dest = updateList.get(i+1);
					if (entityInterceptor != null) {
						childContext.setSrcPropertyValueWithoutModify(src);
						childContext.setDestPropertyValueWithoutModify(dest);
						entityInterceptor.execute(childContext);
					}
					ModelUtil.deepCopy(src, dest, scannerContext, this, identityMap);
				}
			}
			
		} else {
			// 3. update model object (TODO: ONLY copy the change field? how to process if it's changed by the backend service)
			scannerFlag = copyModel(scannerContext);
		}
		return scannerFlag;
	}
	
	private ScannerFlag copyModel(ScannerContext scannerContext) {
		Object srcValue = scannerContext.getSrcPropertyValue();
		ScannerFlag scannerFlag = ScannerFlag.SCANSIBLING;
		Object destValue = scannerContext.getDestPropertyValue();
		
		Object fromId = getId(srcValue);
		Object destId = destValue == null ? null : getId(destValue);
		// TODO: use 'enabledFields' to decide if keeping the original value? 
		if (fromId == null && !isChangedNullValue(srcValue, scannerContext.getChildPath() + ".id")) {
			fromId = destId;		// the id is not enabled to changed, use the original value
		}
		
		// src id is different with the dest id, it means the dest model is changed 
		if (!equals(fromId, destId)) {							// we need load the matched model, and copy each sub property
			// 1) try to find the matched model
			Object unwrapedDestValue = null;
			if (fromId != null) {
				// try to find the existed model from db
				Object existed ;
				if (srcValue instanceof HibernateProxy) {
                    existed = srcValue;
                } else {
                    existed = genericService.load(ClassUtil.getClass(srcValue), fromId);
                }
			}
			
			// 2) no matched model, the dest model is itself (input model)
			if (unwrapedDestValue == null) {
				unwrapedDestValue = updateReference(srcValue, scannerContext);
				// the dest id still use the original dest value, so, the sub child fields still need scan again
				// destId = destId;
			}
			scannerContext.setDestPropertyValue(unwrapedDestValue);
			
			if (equals(destId, fromId) && srcValue != null && !HibernateProxy.class.isAssignableFrom(srcValue.getClass())) {
				scannerFlag = ScannerFlag.SCANCHILD;
			}
		} else if (srcValue != null && destValue == null) {		// dest value is null, use the src value directly
			Object unwrapedDestValue = updateReference(srcValue, scannerContext);
			scannerContext.setDestPropertyValue(unwrapedDestValue);
		} else {
			// src is different with the dest and src is not hibernate object
			if (srcValue != null && !(srcValue instanceof HibernateProxy) && srcValue != destValue) {
				scannerFlag = ScannerFlag.SCANCHILD;
			}
		}
		
		return scannerFlag;
	}
	
	private boolean equals(Object obj1, Object obj2) {
		return ObjectUtils.equals(obj1, obj2);
		//return (obj1 == null && obj2 == null) || (obj1 != null && obj1.equals(obj2));
	}
	
	private Object updateReference(Object value, ScannerContext parentContext) {
		if (value == null) {
			return null;
		}
		
		Object newValue = value;
		
		// update the reference automatically
		ModelScanner.getInstance().scan(value, newValue, parentContext, referenceScannerCallback, identityMap);
		return newValue;
	}
	

	private Collection<Object> caculateCollection(ScannerContext scannerContext, Collection<Object> fromColl, Collection<Object> destColl, final List<Object> updateList) {
		if (null == destColl) {
			destColl = new ArrayList<Object>(fromColl.size());
			for (Object element : fromColl) {
				// TODO: WHY? it's not a copy
				destColl.add(updateReference(element, scannerContext));
			}
		} else {
			List<Object> destIds = new ArrayList<Object>();
			List<Object> destModels = new ArrayList<Object>();
			initInfo(destColl, destIds, destModels);
			
			List<Object> fromIds = new ArrayList<Object>();
			List<Object> fromModels = new ArrayList<Object>();
			initInfo(fromColl, fromIds, fromModels);
			
			OneToMany oneToMany = scannerContext.getPropertyDescriptor().getReadMethod().getAnnotation(OneToMany.class);
			boolean deleteChildRelationship = deleteChildRelationship(oneToMany);
			// 2.1 remove the missed object
			for (int i = 0, len = destIds.size(); i < len; i++) {
				Object destId = destIds.get(i);
				
				int index = fromIds.indexOf(destId);
				if (index < 0) {
					Object destModel = destModels.get(i);
					
					if (deleteChildRelationship) {
						try {
							PropertyUtils.setProperty(destModel, oneToMany.mappedBy(), null);
						} catch(Exception ex) {
							LOGGER.error("Failed to clean target value", ex);
						}
					}
					destColl.remove(destModel);
					// TODO: remove it from the parent reference
					
				}
			}
			
			
			// 2.2 compare the dest list according the id information
			for (int i = 0, len = fromIds.size(); i < len; i++) {
				Object fromId = fromIds.get(i);
				Object fromModel = fromModels.get(i);
				// 2.2.1 add the new model object
				if (fromId == null) {
					destColl.add(updateReference(fromModel, scannerContext));
				} else {
					int index = destIds.indexOf(fromId);
					// 2.2.2 update the existed model
					if (index >= 0) {
						// from
						updateList.add(fromModel);
						// to
						updateList.add(destModels.get(index));
					
						// 2.2.3 remove the missed model
					} else {
						// found the new wrong model id, maybe, it'added by the back-end service or it comes from the dropdown list
						if (!(fromModel instanceof HibernateProxy)) {
							Object dbFromModel = genericService.load(ClassUtil.getClass(fromModel), fromId);
							if (dbFromModel != fromModel) {
								// TODO: security issue? use the security filter to filter the input parameter
								ModelUtil.copyIgnoreNullValue(fromModel, dbFromModel);
							}
							fromModel = dbFromModel;
						}
						destColl.add(updateReference(fromModel, scannerContext));
					}
				}
			}
		}
		return destColl;
	}
	
	private boolean deleteChildRelationship(OneToMany oneToMany) {
		if (oneToMany == null) {
			return false;
		}
		if (oneToMany.orphanRemoval()) {		// hibernate will delete it
			return false;
		}
		if (oneToMany.mappedBy() == null) {
			return false;
		}
		
		CascadeType[] cascades = oneToMany.cascade();
		for (CascadeType cascade : cascades) {
			if (cascade.compareTo(CascadeType.ALL) == 0) {
				return true;
			}
		}
		return false;
	}
	
	private boolean isChangedNullValue(Object model, String propertyName) {
		return changedFields.contains(propertyName);
	}
	
	private void cleanTargetValue(ScannerContext scannerContext) {
		// for the null value, check if the value can be updated to null
		if (isChangedNullValue(scannerContext.getSrcModel(), scannerContext.getChildPath())) {
			// the value is updated to null
			if (scannerContext.isCollection()) {
				Collection<?> coll = (Collection<?>) scannerContext.getDestPropertyValue();
				if (coll != null && !coll.isEmpty()) {
					for (Iterator iter = coll.iterator(); iter.hasNext();) {
						Object element = iter.next();
						iter.remove();
					}
				}
			} else {
				scannerContext.setDestPropertyValue(null);	// set the field to null
			}
		}
	}
	

	private static Object getId(Object model) {
		try {
			return PropertyUtils.getSimpleProperty(model, "id");
		} catch (Exception e) {
			return null;
		}
	}
	
	@SuppressWarnings("unchecked")
	private static void initInfo(Collection<Object> coll, final List<Object> ids, final List<Object> objects) {
		if (null != coll) {
			for (Object model : coll) {
				ids.add(getId(model));
				objects.add(model);
			}
		}
	}
}